import { BlockPoint } from "../model/BlockPoint.js";
import { Nullable } from "../model/types.js";
import { Block, BlockFactory } from "./Block.js";
import { Eraser } from "./Eraser.js";
import { Eventable } from "./Eventable.js";
import { Matrix } from "./Matrix.js";

const factory = BlockFactory.default;

export class BlockController extends Eventable {
    #next: Nullable<Block> = null;
    #current: Nullable<Block> = null;
    #matrix: Matrix;

    constructor(width: number, height: number) {
        super();
        this.#matrix = new Matrix(width, height);
    }

    get current(): Nullable<Block> {
        return this.#current;
    }

    set current(value: Nullable<Block>) {
        this.#current = value;
        this.trigger("currentChanged", { block: value });
    }

    get next(): Block {
        return this.#next || (this.#next = factory.create());
    }

    set next(value: Block) {
        this.#next = value;
        this.trigger("nextChanged", { block: value });
    }

    run(): void {
        this.next = factory.create();
        this.process();
    }

    restart(): void {
        this.#matrix.reset();
        this.trigger("reset");
        this.current = null;
        this.render();
        this.process();
    }

    process(): void {
        if (!this.current) {
            this.processNextShape();
        } else {
            this.processFallDown();
        }
    }

    processNextShape(): void {
        const block = this.current = this.next;
        this.next = factory.create();
        this.render();
        this.current.setPosition(~~((this.#matrix.width - block.width) / 2), -block.height);
        this.processFallDown();
    }

    processFallDown(): void {
        if (!this.current) {
            return;
        }
        const current = this.current;
        const blocks = current.fastenOffset(0, 1);
        if (!this.#matrix.isPutable(blocks)) {
            this.fastenCurrent();
            return;
        }

        current.moveDown();
        this.render();
    }

    fastenCurrent(): void {
        if (!this.current) {
            return;
        }
        this.trigger("fastening");

        if (this.current.top < 0) {
            this.render();
            this.trigger("gameover");
            return;
        }

        this.#matrix.addBlockPoints(this.current.fasten());
        this.current = null;

        const eraser = new Eraser({
            matrix: this.#matrix,
            render: () => this.render(true)
        });

        (async () => {
            if (eraser.check()) {
                const { rowsCount } = eraser;
                this.trigger("earsingRows", { rowsCount });
                await eraser.erase();
                this.trigger("erasedRows", { rowsCount });
            }

            this.render(true);
            this.trigger("fastened");
            this.render();
        })();

    }

    render(shouldFasten: boolean = false): void {
        this.trigger("render", { shouldFasten });
    }

    fastenMatrix(): BlockPoint[] {
        return this.#matrix.fasten();
    }

    rotate(): void {
        const { width, height } = this.#matrix;
        const current = this.current;
        if (!current || !this.#matrix.isPutable(current.fastenRotate(width, height))) {
            return;
        }

        current.rotate(width, height);
        this.render();
    }

    moveLeft(): void {
        const current = this.current;
        if (!current || !this.#matrix.isPutable(current.fastenOffset(-1, 0))) {
            return;
        }

        current.moveLeft();
        this.render();
    }

    moveRight(): void {
        const { width } = this.#matrix;
        const { current } = this;
        if (!current || !this.#matrix.isPutable(current.fastenOffset(1, 0))) {
            return;
        }

        current.moveRight(width);
        this.render();
    }

    moveDown(): void {
        const { height } = this.#matrix;
        const { current } = this;
        if (!current || !this.#matrix.isPutable(current.fastenOffset(0, 1))) {
            return;
        }

        current.moveDown(height);
        this.render();
    }

    fallDownDirectly(): void {
        const { current } = this;
        if (!current) { return; }

        const { left } = current;
        const { height } = this.#matrix;
        for (let top = current.top + 1; top <= height; top++) {
            if (!this.#matrix.isPutable(current.fasten(left, top))) {
                current.setPosition(left, top - 1);
                break;
            }
        }

        this.render();
        this.fastenCurrent();
    }
}
